package org.gwt.grin.client.input;

import org.gwt.grin.client.Feature;
import org.gwt.grin.client.Segment;
import org.gwt.grin.client.Show;
import org.gwt.grin.client.commands.Command;
import org.gwt.grin.client.features.Assembly;
import org.gwt.grin.client.util.Rectangle;

import com.google.gwt.core.client.GWT;

/**
 * A VisualRCHandler models interaction with the remote control and the mouse as
 * a grid of cells. Each cell can contain a state, but it can instead contain an
 * address of another cell. When the user navigates from one cell to another, he
 * lands on the indicated state, or if he lands on a cell that contains the
 * address of another cell, he goes to that cell. A given state can only occupy
 * one cell.
 * <p>
 * No wrapping behavior is specified: If a user tries to navigate off the edge
 * of the grid, the state remains unchanged. If that isn't the desired UI
 * behavior, then simply create a grid where the border consists of cell
 * addresses.
 * <p>
 * A visual handler may optionally be tied to an assembly. When this handler is
 * activated, the handler is put into the state determined by the assembly, by
 * finding the state that corresponding to the assembly's currently active part.
 * Once a handler is active, it assumes that it's the only one in control for
 * the assembly; it doesn't check for the assembly changing state out from under
 * it.
 * <p>
 * A handler can also optionally have commands associated with its states. These
 * are invoked only when the handler changes state - a command is <i>not</i>
 * sent for the current state when the handler is activated. When commands are
 * used to maintain the scene graph's state, it's up to the application to
 * ensure that the UI is in a state that matches the handler's state before the
 * handler is activated.
 * <p>
 * The state of a handler is determined by the activated flag, and by the named
 * state. It may be safely changed with a set_visual_rc command. Once the
 * handler is activated, it stays in that state until reset by a set_visual_rc
 * command, a call to setState(...), or the user navigating with the arrow keys.
 * If the handler is activated and the user presses the enter key again, the
 * handler is "re-activated;" the activation commands are executed again, and
 * the assembly's state is set to selected then immediately to activated, in
 * order to re-run any activation animation.
 * 
 * 
 * @author Bill Foote (http://jovial.com)
 * @author ggeorg
 */
public class VisualRCHandler extends RCHandler {

	/** A special value in a grid tha means to activate the current state. */
	public final static int GRID_ACTIVATE = 0xffff;

	public static int MASK = RCKeyEvent.KEY_UP.getBitMask()
			| RCKeyEvent.KEY_DOWN.getBitMask()
			| RCKeyEvent.KEY_RIGHT.getBitMask()
			| RCKeyEvent.KEY_LEFT.getBitMask()
			| RCKeyEvent.KEY_ENTER.getBitMask();

	/**
	 * For each state, the most significant 16 bits contains the state to go to
	 * on "up", and the least significant 16 bits the "down" value. If this has
	 * the special value GRID_ACTIVATE, then there's no movement, but the
	 * feature is activated.
	 */
	protected int[] upDown;

	/**
	 * For each state, the most significant 16 bits contains the state to go to
	 * on "right", and the least significant 16 bits the "left" value. If this
	 * has the special value GRID_ACTIVATE, then there's no movement, but the
	 * feature is activated.
	 */
	protected int[] rightLeft;

	protected int[][] upDownAlternates; // alternate grid,
	protected int[][] rightLeftAlternates; // cf. visual_grid_alternate
	protected String[] gridAlternateNames; // non-null, can be length 0

	protected String[] stateNames; // The names corresponding to state numbers.
	protected Assembly assembly; // can be null
	protected Feature[] selectFeatures; // By state #, array can be null, and
	// any element can be null.
	protected Command[][] selectCommands; // By state #, array can be null, and
	// any element can be null.
	protected Feature[] activateFeatures; // by state #, etc.
	protected Command[][] activateCommands; // by state #
	protected Rectangle[] mouseRects; // hit zones on screen for the mouse
	protected int[] mouseRectStates; // The state # corresponding to each rect
	protected int timeout; // -1 means "no timeout"
	protected Command[] timeoutCommands;

	/**
	 * Flag that, if true, makes the handler start out in the selected state.
	 * When the handler is activated, if the underlying assembly is found to be
	 * in an activated state, it is coerced into the corresponding selected
	 * state.
	 **/
	protected boolean startSelected = false;

	private boolean activated = false;
	private int currState = 0;
	private int currFrame;
	private boolean timedOut;

	public VisualRCHandler(String name) {
		super();
		this.name = name;
	}

	public void setup(String[] gridNames, String[] stateNames,
			int[][] upDownAlternates, int[][] rightLeftAlternates,
			Command[][] selectCommands, Command[][] activateCommands,
			Rectangle[] mouseRects, int[] mouseRectStates, int timeout,
			Command[] timeoutCommands, boolean startSelected,
			Assembly assembly, Feature[] selectFeatures,
			Feature[] activateFeatures) {
		this.gridAlternateNames = gridNames;
		this.stateNames = stateNames;
		this.upDownAlternates = upDownAlternates;
		this.upDown = upDownAlternates[0];
		this.rightLeftAlternates = rightLeftAlternates;
		this.rightLeft = rightLeftAlternates[0];
		this.selectCommands = selectCommands;
		this.activateCommands = activateCommands;
		this.mouseRects = mouseRects;
		this.mouseRectStates = mouseRectStates;
		this.timeout = timeout;
		this.timeoutCommands = timeoutCommands;
		this.startSelected = startSelected;
		this.assembly = assembly;
		this.selectFeatures = selectFeatures;
		this.activateFeatures = activateFeatures;
	}

	/**
	 * Give a developer-friendly string describing this handler. Useful for
	 * development.
	 **/
	public String toString() {
		return super.toString() + "(" + getName() + ")";
	}

	private boolean handlesActivation() {
		return activateFeatures != null || activateCommands != null;
	}

	/**
	 * This is intended for applications that wish to query the UI state. The
	 * value of this will not be changed as long as a lock is held on our show.
	 **/
	public boolean getActivated() {
		return activated;
	}

	/**
	 * This is intended for applications that wish to query the UI state. The
	 * value of this will not be changed as long as a lock is held on our show.
	 **/
	public int getState() {
		return currState;
	}

	/**
	 * Get the name of a numbered state.
	 **/
	public String getStateName(int stateNum) {
		return stateNames[stateNum];
	}

	/**
	 * Lookup a state number by name. Used for parsing, or by xlets.
	 * 
	 * @return -1 if not found
	 **/
	public int lookupState(String name) {
		for (int i = 0; i < stateNames.length; i++) {
			if (stateNames[i].equals(name)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Look up an alternate grid by name. Used for parsing, or by xlets.
	 * 
	 * @return -1 if not found
	 **/
	public int lookupGrid(String gridAlternateName) {
		for (int i = 0; i < gridAlternateNames.length; i++) {
			if (gridAlternateNames[i].equals(gridAlternateName)) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * {@inheritDoc}
	 **/
	public boolean handleKeyPressed(RCKeyEvent ke, Show caller) {
		if ((ke.getBitMask() & MASK) == 0) {
			return false;
		}
		synchronized (show) {
			int newState = 0;
			if (ke == RCKeyEvent.KEY_ENTER) {
				newState = GRID_ACTIVATE;
			} else if (ke == RCKeyEvent.KEY_UP) {
				newState = (upDown[currState] >> 16) & 0xffff;
			} else if (ke == RCKeyEvent.KEY_DOWN) {
				newState = upDown[currState] & 0xffff;
			} else if (ke == RCKeyEvent.KEY_RIGHT) {
				newState = (rightLeft[currState] >> 16) & 0xffff;
			} else if (ke == RCKeyEvent.KEY_LEFT) {
				newState = rightLeft[currState] & 0xffff;
			} // else if (Debug.ASSERT) {
				// Debug.assertFail();
			// }
			if (newState == GRID_ACTIVATE) {
				if (!handlesActivation()) {
					return false;
				}
				setState(-1, true, true);
				return true;
			} else {
				setState(newState, false, true);
				return true;
			}
		}
	}

	@Override
	public boolean handleKeyReleased(RCKeyEvent ke, Show caller) {
		// ignored
		return false;
	}

	// @Override
	// public boolean handleKeyTyped(RCKeyEvent ke, Show caller) {
	// return false;
	// }

	@Override
	public boolean handleMouse(int x, int y, boolean activate) {
		if (mouseRects == null) {
			return false;
		}
		// Mouse events probably only occur on pretty high-end
		// players, and there probably aren't may rects, so a
		// simple linear search is fine.
		for (int i = 0; i < mouseRects.length; i++) {
			if (mouseRects[i].contains(x, y)) {
				setState(mouseRectStates[i], activate, true);
				return true;
			}
		}
		return false;
	}

	/**
	 * Called from InvokeVisualCellCommand, and from internal methods. This is
	 * synchronized on our show, to only occur during model updates. This method
	 * may also be called from a java_command or from the director, within the
	 * animation thread.
	 * 
	 * @param newState
	 *            New state, -1 means "current"
	 * @param newActivated
	 *            New value for activated
	 * @param runCommands
	 *            If true, run the commands normally associated with entering
	 *            this state due to a keypress.
	 **/
	public void setState(int newState, boolean newActivated, boolean runCommands) {
		setState(newState, newActivated, runCommands, -1);
	}

	/**
	 * Called from InvokeVisualCellCommand, and from internal methods. This is
	 * synchronized on our show, to only occur during model updates. This method
	 * may also be called from a java_command or from the director, within the
	 * animation thread.
	 * 
	 * @param newState
	 *            New state, -1 means "current"
	 * @param newActivated
	 *            New value for activated
	 * @param runCommands
	 *            If true, run the commands normally associated with entering
	 *            this state due to a keypress.
	 * @param gridAlternate
	 *            Alternate grid # to select, 0..max, or -1 to leave grid
	 *            unchanged.
	 **/
	public void setState(int newState, boolean newActivated,
			boolean runCommands, int gridAlternate) {
		synchronized (show) {
			if (gridAlternate != -1) {
				upDown = upDownAlternates[gridAlternate];
				rightLeft = rightLeftAlternates[gridAlternate];
			}
			if (newState == GRID_ACTIVATE) {
				newState = currState;
				newActivated = true;
			} else if (newState == -1) {
				newState = currState;
			}
			if (newState == currState && newActivated == activated) {
				if (activated) {
					// If activated, re-run any animations by
					// briefly setting the assembly to the selected
					// state.
					setState(newState, false, false);
				} else {
					return;
				}
			}
			if (GWT.isClient()) {
				GWT.log("RC handler state becomes " + stateNames[newState]);
			}
			Feature[] fs = newActivated ? activateFeatures : selectFeatures;
			Command[][] cs = newActivated ? activateCommands : selectCommands;
			if (fs != null && fs[newState] != null) {
				assembly.setCurrentPart(fs[newState]);
				if (GWT.isClient()) {
					GWT.log("    Setting assembly to " + fs[newState]);
				}
			}
			if (runCommands && cs != null) {
				Command[] arr = cs[newState];
				if (arr != null) {
					for (int i = 0; i < arr.length; i++) {
						show.runCommand(arr[i]);
					}
				}
			}
			currState = newState;
			activated = newActivated;
		} // end synchronized
	}

	@Override
	public void activate(Segment s) {
		timedOut = timeout <= -1;
		currFrame = 0;
		if (assembly != null) {
			// If we have an assembly, make our state mirror
			// that of the assembly.
			Feature curr = assembly.getCurrentPart();
			int i = lookForFeature(curr, selectFeatures);
			if (i != -1) {
				currState = i;
				activated = false;
			} else {
				i = lookForFeature(curr, activateFeatures);
				if (i != -1) {
					currState = i;
					if (startSelected) {
						assembly.setCurrentPart(selectFeatures[i]);
						activated = false;
					} else {
						activated = true;
					}
				} else if (GWT.isClient()) {
					GWT.log("Handler " + getName()
							+ " can't find current assembly state");
				}
			}
		}
	}

	private int lookForFeature(Feature f, Feature[] fs) {
		if (fs == null) {
			return -1;
		}
		for (int i = 0; i < fs.length; i++) {
			if (fs[i] == f) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * {@inheritDoc}
	 **/
	public void nextFrame() {
		currFrame++;
		if (!timedOut && currFrame > timeout) {
			timedOut = true;
			for (int i = 0; i < timeoutCommands.length; i++) {
				show.runCommand(timeoutCommands[i]);
			}
		}
	}
}
